package com.dbflow5.query;

import com.dbflow5.StringUtils;
import com.dbflow5.config.FlowManager;
import com.dbflow5.query.property.IProperty;
import com.dbflow5.query.property.PropertyFactory;
import com.dbflow5.sql.Query;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Description: Specifies a SQLite JOIN statement
 */
public class Join<TModel, TFromModel> implements Query {

    Class<TModel> table;

    /**
     * The type of JOIN to use
     */
    private final JoinType type;

    /**
     * The FROM statement that prefixes this statement.
     */
    private final From<TFromModel> from;

    /**
     * The alias to name the JOIN
     */
    private NameAlias alias;

    /**
     * The ON conditions
     */
    private OperatorGroup onGroup = null;

    /**
     * What columns to use.
     */
    private final List<IProperty<?>> using = new ArrayList<>();

    @Override// natural joins do no have on or using clauses.
    public String getQuery() {
        StringBuilder queryBuilder = new StringBuilder();

        queryBuilder.append(type.name().replace("_", " ")).append(" ");

        queryBuilder.append("JOIN")
                .append(" ")
                .append(alias.fullQuery())
                .append(" ");
        if (JoinType.NATURAL != type) {
            if(onGroup != null){
                queryBuilder.append("ON")
                        .append(" ")
                        .append(onGroup.getQuery())
                        .append(" ");
            }else {
                if(!using.isEmpty()){
                    queryBuilder.append("USING (");
                    StringUtils.appendList(queryBuilder, using);
                    queryBuilder.append(") ");
                }
            }
        }
        return queryBuilder.toString();
    }

    /**
     * The specific type of JOIN that is used.
     */
    enum JoinType {

        /**
         * an extension of the INNER JOIN. Though SQL standard defines three types of OUTER JOINs: LEFT, RIGHT,
         * and FULL but SQLite only supports the LEFT OUTER JOIN.
         *
         *
         * The OUTER JOINs have a condition that is identical to INNER JOINs, expressed using an ON, USING, or NATURAL keyword.
         * The initial results table is calculated the same way. Once the primary JOIN is calculated,
         * an OUTER join will take any unjoined rows from one or both tables, pad them out with NULLs,
         * and append them to the resulting table.
         */
        LEFT_OUTER,

        /**
         * creates a new result table by combining column values of two tables (table1 and table2) based upon the join-predicate.
         * The query compares each row of table1 with each row of table2 to find all pairs of rows which satisfy the join-predicate.
         * When the join-predicate is satisfied, column values for each matched pair of rows of A and B are combined into a result row
         */
        INNER,

        /**
         * matches every row of the first table with every row of the second table. If the input tables
         * have x and y columns, respectively, the resulting table will have x*y columns.
         * Because CROSS JOINs have the potential to generate extremely large tables,
         * care must be taken to only use them when appropriate.
         */
        CROSS,

        /**
         * a join that performs the same task as an INNER or LEFT JOIN, in which the ON or USING
         * clause refers to all columns that the tables to be joined have in common.
         */
        NATURAL
    }

    public Join(From<TFromModel> from, Class<TModel> table, JoinType joinType) {
        this.from = from;
        this.table = table;
        type = joinType;
        alias = new NameAlias.Builder(FlowManager.getTableName(table)).build();
    }

    public Join(From<TFromModel> from, JoinType joinType, ModelQueriable<TModel> modelQueriable) {
        table = modelQueriable.table();
        this.from = from;
        type = joinType;
        alias = PropertyFactory.from(modelQueriable).nameAlias();
    }

    /**
     * Specifies if the JOIN has a name it should be called.
     *
     * @param alias The name to give it
     * @return This instance
     */
    public Join<TModel, TFromModel> as(String alias) {
        this.alias = this.alias
                .newBuilder()
                .as(alias)
                .build();
        return this;
    }

    /**
     * Specify the conditions that the JOIN is on
     *
     * @param onConditions The conditions it is on
     * @return The FROM that this JOIN came from
     */
    public From<TFromModel> on(SQLOperator... onConditions) {
        checkNatural();
        onGroup = OperatorGroup.nonGroupingClause().andAll(onConditions);
        return from;
    }

    /**
     * Specify the conditions that the JOIN is on
     *
     * @param sqlOperator The operator that the JOIN is ON.
     * @return The FROM that this JOIN came from
     */
    public From<TFromModel> on(SQLOperator sqlOperator) {
        checkNatural();
        onGroup = OperatorGroup.nonGroupingClause().and(sqlOperator);
        return from;
    }

    /**
     * The USING statement of the JOIN
     *
     * @param columns THe columns to use
     * @return The FROM that this JOIN came from
     */
    public From<TFromModel> using(IProperty<?>... columns) {
        checkNatural();
        using.addAll(Arrays.asList(columns));
        return from;
    }

    /**
     * The USING statement of the JOIN
     *
     * @param property The property its using (singular).
     * @return The FROM that this JOIN came from
     */
    public From<TFromModel> using(IProperty<?> property) {
        checkNatural();
        using.add(property);
        return from;
    }

    /**
     * The END statement of the JOIN
     * @return End this [Join]. Used for [Join.JoinType.NATURAL]
     */
    public From<TFromModel> end() {
        return from;
    }

    private void checkNatural() {
        if (JoinType.NATURAL == type) {
            throw new IllegalArgumentException("Cannot specify a clause for this join if its NATURAL."
                    + " Specifying a clause would have no effect. Call end() to continue the query.");
        }
    }
}
